﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using System.Xml;
using Quail.BinaryCompressedPackageFile;
using QuailDb;

namespace QuailDashboard.View.Web
{
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public partial class QuailTestRunner : IDisposable
    {
        private static readonly object SyncRoot = new object();
        private static readonly Dictionary<string, RunLauncherState> RunStateDictionary = new Dictionary<string, RunLauncherState>();
        private static Timer _cleanupTimer;
        private QuailObjectContextConnection _dbContext;
        private QuailDataContext _db;


        public QuailTestRunner()
        {
            _dbContext = new QuailObjectContextConnection();
            _db = _dbContext.Context;
        }

        ~QuailTestRunner()
        {
            Dispose(false);
        }

        /// <summary>
        /// calls Dispose(true)
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes the database connection
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                _db = null;

                if (_dbContext != null)
                {
                    _dbContext.Dispose();
                    _dbContext = null;
                }
            }
        }

        /// <summary>
        /// Returns true if the test is still running
        /// </summary>
        /// <param name="testrunid">The testrunid returned from RunTestPackageLibrary</param>
        /// <returns></returns>
        [OperationContract]
        public bool IsTestRunning(string testrunid)
        {
            var state = GetRunState(testrunid);

            return state != null && state.IsRunning;
        }

        /// <summary>
        /// Returns test test run results
        /// </summary>
        /// <param name="testrunid">The testrunid returned from RunTestPackageLibrary</param>
        /// <param name="logtext">The Gallio log text</param>
        /// <returns></returns>
        [OperationContract]
        public string TestRunResults(string testrunid, out string logtext)
        {
            var state = GetRunState(testrunid);

            if (state == null)
            {
                logtext = string.Empty;
                return "Test results are not known!";
            }

            logtext = state.LogText;
            return state.ResultSummary;
        }

        /// <summary>
        /// Runs a test package with specific options
        /// </summary>
        /// <param name="packagename">The name of the package</param>
        /// <param name="version">The version number</param>
        /// <param name="options">Options</param>
        /// <returns>The test run ID</returns>
        [OperationContract]
        public string RunTestPackageLibrary(string packagename, string version, TestRunOptions options)
        {
            if (string.IsNullOrEmpty(options.DefaultEnvironment))
                throw new Exception("options.DefaultEnvironment must be defined");

            if (options.TestAssemblies == null || options.TestAssemblies.Count == 0)
                throw new Exception("options.TestAssemblies must contain at least one assembly");

            var rootfolder = ConfigurationManager.AppSettings["TestFolder"];
            if (rootfolder == null || !Directory.Exists(rootfolder))
                throw new Exception("The TestFolder [" + (rootfolder ?? "NULL") + "] doesn't exist!");

            if (!rootfolder.EndsWith(@"\"))
                rootfolder += @"\";
            
            var outputfolder = rootfolder + DateTime.Now.ToString("yyyyMMdd.HHmmss.") + ToValidFilename(packagename) + "." +
                           ToValidFilename(version);

            if (Directory.Exists(outputfolder))
                outputfolder += "." + ToValidFilename(Guid.NewGuid().ToString());

            Directory.CreateDirectory(outputfolder);

            outputfolder += @"\";

            var deleteoutputfolder = true;

            try
            {
                var files = GetBinaryPackageFiles(packagename, version);

                // make sure the assemblies are in there
                options.TestAssemblies.ForEach(asm =>
                                                   {
                                                       if (
                                                           !files.Any(
                                                               bcpf => string.Compare(bcpf.Filename, asm, true) == 0))
                                                           throw new Exception("Unable to locate test assembly: " +
                                                                               asm);
                                                   });

                // find the (optional) config file for each matching assembly and change the options in it
                options.TestAssemblies.ForEach(asm =>
                                                   {
                                                       var configname = Path.GetFileName(asm) + ".config";
                                                       var configfile =
                                                           files.Where(
                                                               bcpf =>
                                                               string.Compare(bcpf.Filename, configname, true) == 0)
                                                               .
                                                               SingleOrDefault();

                                                       if (configfile != null)
                                                           UpdateConfigFile(configfile, options, outputfolder);
                                                   });

                // now write out the files
                files.ForEach(bcpf =>
                                  {
                                      using (
                                          var file = new FileStream(outputfolder + bcpf.Filename, FileMode.CreateNew,
                                                                    FileAccess.Write))
                                      {
                                          var raw = bcpf.GetUncompressed();
                                          file.Write(raw, 0, raw.Length);
                                      }
                                  });

                // kick off the test runner
                var state = new RunLauncherState(outputfolder, options);

                foreach (var launcher in state.Launchers)
                    launcher.Start();
                // ThreadPool.QueueUserWorkItem(RunLauncher, launcher);
                deleteoutputfolder = false;

                AddRunState(state);

                return state.MyKey;
            }
            finally
            {
                // delete the output folder if something went wrong
                if (deleteoutputfolder)
                    RemoveFolder(outputfolder);
            }
        }
        

        private static string ToValidFilename(IEnumerable<char> str)
        {
            var retVal = string.Empty;
            var invalids = Path.GetInvalidFileNameChars();

            return str.Where(c => !invalids.Contains(c)).Aggregate(retVal, (current, c) => current + c);
        }

        /// <summary>
        /// Cleans up the test run dictionary
        /// </summary>
        /// <param name="state"></param>
        private static void CleanupTestRuns(object state)
        {
            lock (SyncRoot)
            {
                var keys = RunStateDictionary.Keys.ToList();

                foreach (var key in keys)
                {
                    var item = RunStateDictionary[key];
                    if (item.ReadyToExpire)
                    {
                        RunStateDictionary.Remove(key);
                        item.Dispose();
                    }
                }

                if (RunStateDictionary.Count == 0 && _cleanupTimer != null)
                {
                    var timer = _cleanupTimer;
                    _cleanupTimer = null;
                    timer.Dispose();
                }
            }
        }

        private static void AddRunState(RunLauncherState runstate)
        {
            lock (SyncRoot)
            {
                RunStateDictionary[runstate.MyKey] = runstate;
                if (_cleanupTimer == null)
                    _cleanupTimer = new Timer(CleanupTestRuns, null, 20000, 20000);
            }
        }

        private static RunLauncherState GetRunState(string runid)
        {
            lock (SyncRoot)
            {
                RunLauncherState result;
                return RunStateDictionary.TryGetValue(runid, out result) ? result : null;
            }
        }

        private class RunLauncherState : IDisposable
        {
            public RunLauncherState(string outputfolder, TestRunOptions options)
            {
                if (string.IsNullOrEmpty(outputfolder))
                    throw new ArgumentException("Outputfolder is null or empty", outputfolder);

                if (options == null)
                    throw new ArgumentNullException("options");

                MyKey = Guid.NewGuid().ToString();

                OutputFolder = outputfolder;
                Options = options;

                var filter = string.Empty;

                // See: http://www.gallio.org/api/html/P_Gallio_MSBuildTasks_Gallio_Filter.htm for examples for filter strings

                // add the test methods to the launcher
                if (options.TestClasses != null && options.TestClasses.Count > 0)
                {
                    var tcfilter = string.Empty;
                    options.TestClasses.ForEach(tc =>
                    {
                        if (tcfilter.Length > 0)
                            tcfilter += ",";
                        tcfilter += tc;
                    });
                    filter += "(Type: " + tcfilter + ")";
                }

                if (options.TestMethods != null && options.TestMethods.Count > 0)
                {
                    var tmfilter = string.Empty;
                    options.TestMethods.ForEach(tm =>
                    {
                        if (tmfilter.Length > 0)
                            tmfilter += " or ";

                        var split = tm.Split('.');

                        var method = split.Last();
                        var classname = tm.Substring(0, tm.Length - method.Length - 1);

                        tmfilter += "(Type: " + classname + " and Member: " + method + ")";
                    });

                    if (filter.Length > 0)
                        filter += " or ";
                    filter += tmfilter;
                }

                Launchers = new List<TestProcessAndResult>();

                var httpc = HttpContext.Current;
                if (httpc == null)
                    throw new Exception("HttpContext.Current is null");

                var server = httpc.Server;
                if (server == null)
                    throw new Exception("HttpContext.Current.Server is null");

                var echopath = GetEchoPath(server) + "Gallio.Echo.Exe";
                
                //var performanceTest = new Quail.PerformanceTest();
                //if (options.CustomOverrides != null)
                //{
                //    if (options.CustomOverrides.ContainsKey("NumberOfTests"))
                //        performanceTest.NumberOfTests = int.Parse(options.CustomOverrides["NumberOfTests"]);
                //    if (options.CustomOverrides.ContainsKey("NumberOfThreads"))
                //        performanceTest.NumberOfThreads = int.Parse(options.CustomOverrides["NumberOfThreads"]);
                //}

                //var serializer = new System.Xml.Serialization.XmlSerializer(performanceTest.GetType());
                //using (var configWriter = new StreamWriter(outputfolder + "PerformanceTest.xml", false, Encoding.UTF8))
                //{
                //    serializer.Serialize(configWriter, performanceTest);
                //}

                options
                    .TestAssemblies
                    .ForEach(asm =>
                                 {
                                     var parmsfile = OutputFolder + ToValidFilename(Guid.NewGuid().ToString()) + ".txt";
                                     using (var writer = new StreamWriter(parmsfile, false, Encoding.ASCII))
                                     {
                                         writer.Write("\"{0}\"", OutputFolder + asm);

                                         if (!string.IsNullOrEmpty(filter))
                                             writer.Write(" \"/filter:{0}\"", filter);
                                     }

                                     var psi = new ProcessStartInfo(echopath, "@" + Path.GetFileName(parmsfile))
                                                   {
                                                       RedirectStandardOutput = true,
                                                       WorkingDirectory = Path.GetDirectoryName(parmsfile)
                                                   };

                                     var tpr = new TestProcessAndResult(psi, OutputFolder);
                                     tpr.OnComplete += OnTestProcessComplete;
                                     Launchers.Add(tpr);
                                 }
                    );

                StartTime = DateTime.Now;
            }

            public List<TestProcessAndResult> Launchers
            {
                get;
                private set;
            }

            public DateTime StartTime
            {
                get; private set;
            }

            public bool IsRunning
            {
                get
                {
                    return Launchers.Count > 0 && Launchers.Any(l => l.IsRunning);
                }
            }

            public bool ReadyToExpire
            {
                get
                {
                    return (!IsRunning && StartTime < DateTime.Now.AddHours(-1)) || StartTime < DateTime.Now.AddHours(-12);
                }
            }

            public string MyKey
            {
                get;
                private set;
            }

            public string LogText
            {
                get
                {
                    var result = string.Empty;
                    Launchers.ForEach(tlr => result += tlr.Output ?? string.Empty);

                    return result;
                }
            }

            public string ResultSummary
            {
                get
                {
                    var types = new List<string>
                                        {
                                            "run",
                                            "passed",
                                            "failed",
                                            "inconclusive",
                                            "skipped"
                                        };

                    using (var reader = new StringReader(LogText))
                    {
                        string line;

                        while ((line = reader.ReadLine()) != null)
                        {
                            if (types.All(t => line.Contains(t)))
                                return line;
                        }
                    }

                    return string.Empty;
                }
            }

            public void Dispose()
            {
                Launchers.ForEach(tpr => tpr.Dispose());
                Launchers.Clear();
            }

            private string OutputFolder
            {
                get; set;
            }

            private TestRunOptions Options
            {
                get; set;
            }

            private void OnTestProcessComplete(TestProcessAndResult obj)
            {
                if (!IsRunning)
                    RemoveFolder(OutputFolder);
            }

            private static string AlphaNumericOnly(string str)
            {
                var result = string.Empty;
                foreach (char c in str)
                {
                    if (char.IsLetterOrDigit(c))
                        result += c;
                }

                return result;
            }

            private static object SyncStatic = new object();
            private static string WorkingEchoFolder;
            private static string GetEchoPath(HttpServerUtility server)
            {
                lock (SyncStatic)
                {
                    if (WorkingEchoFolder != null)
                        return WorkingEchoFolder;

                    var gfolder = ConfigurationManager.AppSettings["GallioInstallFolder"];

                    if (string.IsNullOrEmpty(gfolder))
                        throw new Exception("GallioInstallFolder is null or empty");

                    var echopath = Path.IsPathRooted(gfolder) ? gfolder : server.MapPath(gfolder);
                    if (echopath == null)
                        throw new Exception("GallioInstallFolder not defined!");

                    var existinghash = GetFolderHash(echopath);
                        
                    var workingfolder = ConfigurationManager.AppSettings["TestFolder"];

                    // search existing gallio folders to see if one already exists
                    foreach (var folder in Directory.EnumerateDirectories(workingfolder, "Gallio.*"))
                    {
                        var foundhash = GetFolderHash(folder);
                        if (foundhash == existinghash)
                        {
                            // found one - re-use it!
                            WorkingEchoFolder = folder;
                            break;
                        }
                    }

                    if (WorkingEchoFolder == null)
                    {
                        // if we get here, we couldn't find a Gallio folder matching the one in the GallioInstallFolder

                        // now copy to a new path using DatetimeStamp.Gallio
                        if (!workingfolder.EndsWith(Path.DirectorySeparatorChar.ToString()))
                            workingfolder += Path.DirectorySeparatorChar;

                        workingfolder += "Gallio." + DateTime.Now.ToString("yyyyMMdd.HHmmss");
                        // create the new folder
                        Directory.CreateDirectory(workingfolder);

                        // and copy files to it
                        CopyFolder(echopath, workingfolder);

                        WorkingEchoFolder = workingfolder + Path.DirectorySeparatorChar;
                    }
                    else if (!WorkingEchoFolder.EndsWith(Path.DirectorySeparatorChar.ToString()))
                        WorkingEchoFolder = WorkingEchoFolder + Path.DirectorySeparatorChar;
                    return WorkingEchoFolder;
                }
            }

            private static void CopyFolder(string source, string dest)
            {
                Directory.CreateDirectory(dest);
                foreach (var file in Directory.EnumerateFiles(source))
                {
                    File.Copy(file, dest + Path.DirectorySeparatorChar + Path.GetFileName(file));
                }

                foreach (var folder in Directory.EnumerateDirectories(source))
                {
                    CopyFolder(folder, dest + Path.DirectorySeparatorChar + Path.GetFileName(folder));
                }
            }

            private static string GetFolderHash(string folder)
            {
                var hash = SHA256.Create();

                var buf = new byte[10240];
                Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories)
                    .Select(f => f.ToLower())
                    .ToList()
                    .OrderBy(f => f)
                    .ForEach(filename =>
                                 {
                                     using (var file = File.OpenRead(filename))
                                     {
                                         int read;
                                         while ((read = file.Read(buf, 0, buf.Length)) > 0)
                                         {
                                             hash.TransformBlock(buf, 0, read, buf, 0);
                                         }
                                     }
                                 });

                hash.TransformFinalBlock(buf, 0, 0);

                return Convert.ToBase64String(hash.Hash);
            }

        }

        private class TestProcessAndResult : IDisposable
        {
            private readonly ProcessStartInfo _psi;
            private readonly string _outputFolder;
            private Process _p;
            private string _output = string.Empty;
            private int _exitCode = -1;
            private bool _wasrunning;

            public TestProcessAndResult(ProcessStartInfo psi, string outputfolder)
            {
                _psi = psi;
                _outputFolder = outputfolder;
            }

            public event Action<TestProcessAndResult> OnComplete;

            public bool IsRunning
            {
                get
                {
                    var isrunningnow = _p != null && !_p.HasExited;

                    if (_wasrunning && !isrunningnow)
                    {
                        _wasrunning = false;
                        if (_p != null)
                        {
                            _exitCode = _p.ExitCode;
                            _p.Close();
                            _p = null;
                        }

                        FireOnComplete();
                    }

                    return isrunningnow;
                }
            }

            public void Dispose()
            {
                if (_p != null)
                {
                    _p.Close();
                    _p = null;
                }

                RemoveFolder(_outputFolder);
            }

            public void Start()
            {
                _psi.UseShellExecute = false;
                _psi.RedirectStandardOutput = true;
                _output = string.Empty;

                try
                {
                    _p = Process.Start(_psi);

                    if (_p != null)
                    {
                        _p.OutputDataReceived += OnReadOutput;
                        _p.BeginOutputReadLine();
                        _wasrunning = true;
                    }
                    else
                        _output = "Unknown error starting process!";
                }
                catch(Exception e)
                {
                    _output = e.ToString(); 
                    _p = null;
                    _exitCode = -1;

                    FireOnComplete();
                }
            }

            private void OnReadOutput(object sender, DataReceivedEventArgs e)
            {
                if (e.Data != null)
                    _output += e.Data + Environment.NewLine;
            }

            public int ReturnCode
            {
                get
                {
                    return _exitCode;
                }
            }
            
            public string Output
            {
                get
                {
                    return _output;
                }
            }

            private void FireOnComplete()
            {
                if (OnComplete != null)
                    OnComplete(this);
            }
        }

        private static void RemoveFolder(string folder)
        {
            try
            {
                if (Directory.Exists(folder))
                {
                    foreach (var file in Directory.GetFiles(folder))
                        File.Delete(file);
                    Directory.Delete(folder, true);
                }
            }
            catch
            {
                // ignore all exceptions
            }
        }

        private static void UpdateConfigFile(BinaryCompressedPackageFile configfile, TestRunOptions options, string outputfolder)
        {
            var dom = new XmlDocument();
            using (var memstream = new MemoryStream(configfile.GetUncompressed()))
            {
                using (var reader = new StreamReader(memstream, true))
                {
                    dom.LoadXml(reader.ReadToEnd());
                }
            }

            SetValueNode(dom, configfile, "DefaultEnvironment", options.DefaultEnvironment);

            if (options.RemoteControlHost != null && options.RemoteControlPort > 0)
            {
                SetValueNode(dom, configfile, "RemoteControlFromDatabase", "False");
                SetValueNode(dom, configfile, "RemoteControlHost", options.RemoteControlHost);
                SetValueNode(dom, configfile, "RemoteControlPort", options.RemoteControlPort.ToString());
            }
            else
                SetValueNode(dom, configfile, "RemoteControlFromDatabase", "True");

            SetValueNode(dom, configfile, "XmlLoggerFolder", outputfolder);
            configfile.SetUncompressed(Encoding.ASCII.GetBytes(dom.OuterXml));
        }

        private static void SetValueNode(XmlNode dom, BinaryCompressedPackageFile configfile, string name, string newvalue)
        {
            var node = dom.SelectSingleNode(string.Format("//setting[@name='{0}']/value", name));
            if (node != null)
                node.InnerText = newvalue;
        }

    }
}
